Typically, a developer calls numerous APIs in the client role before being given the task of “switching sides” as an API designer, developing an API themselves, and making it available to others. Nowadays, this happens sooner rather than later. It’s impossible to imagine current software practice without Web APIs and service interfaces. Most of the systems we develop are distributed and therefore, need APIs to allow application parts hosted in different places (also in the cloud) to communicate with each other. This applies both to APIs internal to the application (in a microservices architecture) and to APIs from external partners (an online store payment service). By definition, APIs hide implementation details from callers. They allow component mocking during testing and exchanging alternative implementations without immediate impact on users. The service provider’s development and lifecycle is decoupled from the API clients, which makes developing, operating, and maintaining the overall system much more flexible.
As with all challenging issues, there are unfortunately many things that can go wrong with API design. Confusing data structures increase the programming and testing effort on the client side; implementation details in the interface contract impair maintainability; promises an API makes (possibly only implicitly) but doesn’t implement, and a lack of robustness disappoint the API user community.
Consequences of these errors can vary in severity. It ranges from additional implementation costs to misunderstandings and defects that are only found in integration phases and need costly redesigns. It can cause production problems later (performance, scalability) with correction requirements, to expensive architectural changes. The later a bug is found, the higher the cost to fix it.
This raises the question of how API design errors can be avoided from the outset, and how to react effectively and efficiently if, despite all efforts, a design doesn’t match the requirements (which may only emerge in parallel with the design).
The role of patterns in API design
Developing an API is like taking a trip. The first stop on any trip should be at a bookstore (or the Internet) to find a travel guide. Travel guides contain authors’ experiences about places to visit and restaurants to eat. We all learn from experience. It’s often our own mistakes (like a poorly chosen restaurant on vacation) that helps us move forward in the long run (taking a closer look at the menu and ambiance, considering alternatives). But, we also like to continue using good solutions that have been proven through experience (don’t we all have a favorite restaurant?). Of course, we can also benefit from others’ experiences in API design, just like with a guidebook.
Pattern descriptions are a popular way to document software development experiences and make them usable for others. A pattern is a solution pattern for a certain recurring problem. This isn’t about code reuse, but about describing conceptual, cross-technical problems and solutions that address non-trivial, sometimes contradictory requirements.
A look at sources shows there are already patterns that can help API designers with their designs. From Enterprise Integration Patterns [1] to Service Design Patterns [2] to Cloud Computing Patterns [3], valuable experiences with message types, integration styles, and coupling properties have already been extensively documented. With the Microservice API Patterns project [4], a complementary collection of patterns emerged that deals with designing the invocation and response messages exchanged between API provider and client. These patterns can be used to influence API properties reflected in the message content. One example of this is the pagination pattern for splitting large result sets among multiple requests and responses. Many web applications are confronted with this issue. At first glance, the task seems simple. But in implementation and in practice, there are many variants that significantly differ in terms of client convenience and provider overhead. (For example, how does the client navigate from page to page? Does the provider need to remember intermediate states? What happens in case of an error?)
Generally, no single pattern can and will solve all problems. Therefore, individual patterns are often combined. But this doesn’t mean you should enter a competition to see who knows and can apply more patterns. Just like it makes no sense to go on vacation and visit all the well-rated restaurants in the guidebook in one week, forcing yourself to eat fish even though you don’t like it, you can’t improve APIs by applying as many patterns as possible. The opposite is true, since patterns would be applied that don’t help your problem, confusing callers, and potentially even causing the API’s quality to degrade.Instead, the goal is to find a few specific patterns for the current design problems in your project and product context. Pattern descriptions will help you make a decision by making their context explicit, asking a specific question, and discussing quality properties affected by the solution. In the process, certain qualities may be improved and others degraded by using a pattern (for example, access security and data protection can hardly exist without certain configuration and computational efforts). A pattern should be used if the context fits the problem. It addresses the quality properties to be achieved and the deterioration of other quality properties is tolerable. Less is more!
New addition: (Micro) Service API Patterns
The second stop on our API design journey (or preparing for the journey) is a visit to the Microservice API website [4]. Due to their pattern character, the Microservice API Pattern project’s patterns (Fig. 1) are independent from the chosen integration technology, like RESTful HTTP, gRPC, or even queue-based messaging. They present recurring design issues at a conceptual flight level. Quality aspects like developer experience (learnability, understandability) or the influence of message size and number on response times are described in detail for each pattern. Examples form the bridge into implementation, with Java Spring for HTTP and JSON.
Fig. 1: Microservice API patterns categories
By the way, the acronym MAP shows that the pattern collection can support us like an API design “map”. A total of 46 patterns are organized into five categories:
- Foundation: Basic patterns concerning the type and use of an API.
- Responsibility: Responsibilities and viewpoints of API endpoints and their operations (such as activity- or data-oriented role, read and/or write behavior).
- Structure: Options for submitting data formats (e.g. nesting JSON objects and using arrays).
- Quality: Patterns to improve security and performance, balance data volume and message count (should referenced entities be linked or included?), and more.
- Evolution: Patterns for coordinating API evolution between API providers and API clients (e.g. lifecycle management and versioning options).
The patterns are based on experience from our own projects, studying publicly available APIs, and project experience from members in our professional networks and the open source community. “We” in this context means a five-person team that I belong to. We’ve been preparing and documenting the patterns since 2016.
Short trip: Designing a simple API for bank master data
The API design itself can be designed as a journey through different pattern categories. See Figure 2. We’ll describe this with a simple example. Let’s develop an API that allows registered corporate customers to retrieve information about credit institutions, like name, address, and BIC.
Fig. 2: Patterns used in the sample bank data API
First, we select the patterns in the Foundation category that we introduced above. This API is intended to be callable only by registered corporate clients. Therefore, it can be described as a community API with different protection and scaling needs than a public API, for instance. The API is used to allow various software systems to retrieve data in an automated manner. This example is a back-end integration where a few calls are usually sufficient, or even preferable. But it must then be able to robustly handle large amounts of data. Additionally, integrating development teams should be given a detailed description of the API. An API description services as a commercial and technical interface contract.
The next stop is the architecture-relevant responsibility patterns. The API only reads data, but doesn’t modify it. Thus, it implements the retrieval operation pattern. The data is essentially static, since bank master data doesn’t usually change. Therefore, this API is a reference data holder. The API is stateless and can be deployed, scaled, and moved in a cloud.
At first glance, the three foundation patterns and the two responsibility patterns don’t seem imposing or surprising. On the contrary, they even appear self-evident. This may be the case with patterns. They’re supposed to transport secured community knowledge. If you already know this, you’ll hopefully still learn some interesting things about selection criteria and design. The pattern names establish a common vocabulary and can act as a checklist. Furthermore, we’ve also found that it’s helpful to make “self-evident”, only implicit assumptions explicit. This way, you will be able to make better decisions more consciously. Your decisions also concern the Structure, Quality, and Evolution categories, which we’ll encounter next on our short API trip.
We’ve determined that this API is a community API, so we’d like to convey information regarding the call context in our request, like a unique message ID. Let’s use the Context Representation pattern for this. It places the ID in the payload instead of a protocol header (although this can be done as an alternative). We also want the API to only be accessible to a limited set of users, so we need to be able to authenticate and authorize API requests. An API Key provides a simple token-based solution for this. Finally, in order to prevent system overload, we apply the Rate Limit pattern. This limits the number of requests per minute for each partner system. We can maintain the rate limit despite the expected high demand for bank data, since the data in a reference data holder is static.
If the API changes later, at some point, it won’t be possible or even economically feasible to make these changes backwards compatible. For example, with the introduction of the BIC and replacing the bank sort code with external factors, this API would be forced to make non-backward compatible changes. Likewise, the cryptographic functions for transport encryption or API key generation may no longer provide sufficient security and you may need to switch to newer protocols. So it’s important to define from the start which mechanisms should be used to decouple the API client and API provider lifecycle. Our API can use the limited lifetime guarantee pattern here. For example, it can provide planning security for a client for a defined period of time and gives the provider a window of time to make non-backward-compatible changes if needed. By already classifying the API as a community API, we’re aware that we can communicate notifications and lifecycle changes directly to our partners (as we know them).
This concludes our short trip through the pattern language. But why is it even called a pattern language at all? Patterns contain experience and their names facilitate communication in the project. Pagination is well-known to us all. Many complex features from this concept should be familiar. Patterns also serve to unify communication in our project teams and beyond. We invite you to visit the API patterns website. You can find all patterns with some additional motivational arguments, design details, and implementation notes. Not everything is equally (or simultaneously) important in every API. But your API users will thank you for solving design issues and for even making the solutions recognizable and usable without much explanation or documentation efforts by using pattern names in the API documentation.
What’s next?
This article gave an initial overview of our design patterns for message-based APIs. It is the prelude to our journey through the pattern language in this small series of articles.
Microservice API patterns can be used by API designers to solve conceptual design problems independent of the interface technology used. All patterns, including detailed descriptions, are freely available on the project homepage.
In the next part, we’ll continue our journey and introduce different design patterns in the Quality category in more detail. We’ll discuss how different patterns can affect the number and size of exchanged messages.
References
[1] Hohpe, Gregor; Bobby Woolf: “Enterprise integration patterns: Designing, building, and deploying messaging solutions”, Addison-Wesley Professional, 2004.
[2] Daigneau, Robert: “Service Design Patterns: fundamental design solutions for SOAP/WSDL and restful Web Services”, Addison-Wesley, 2012.
[3] Fehling, Christoph, et al.: “Cloud computing patterns: fundamentals to design, build, and manage cloud applications”, Springer Science & Business Media, 2014.
[4] Microservice-API-Patterns, https://www.microservice-api-patterns.org, accessed on 10.06.2022.